package cn.micro.core.util;


import cn.micro.core.exception.BizException;
import net.sf.cglib.beans.BeanMap;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.cglib.beans.BeanGenerator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.FastByteArrayOutputStream;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.*;

/**
 * 实体工具类
 */
public class BeanUtil extends BeanUtils {

    /**
     * 实例化对象
     *
     * @param clazz 类
     * @param <T>   泛型标记
     * @return 对象
     */
    @SuppressWarnings("unchecked")
    public static <T> T newInstance(Class<?> clazz) {
        return (T) instantiateClass(clazz);
    }

    /**
     * 实例化对象
     *
     * @param clazzStr 类名
     * @param <T>      泛型标记
     * @return 对象
     */
    public static <T> T newInstance(String clazzStr) {
        return newInstance(forName(clazzStr));
    }

    /**
     * forName
     *
     * @param clazzStr 类名
     * @return Class
     */
    public static Class<?> forName(String clazzStr) {
        try {
            return ClassUtils.forName(clazzStr, null);
        } catch (ClassNotFoundException e) {
            throw new BizException(e);
        }
    }

    /**
     * 获取Bean的属性, 支持 propertyName 多级 ：test.user.name
     *
     * @param bean         bean
     * @param propertyName 属性名
     * @return 属性值
     */
    @Nullable
    public static Object getProperty(@Nullable Object bean, String propertyName) {
        if (bean == null) {
            return null;
        }
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
        return beanWrapper.getPropertyValue(propertyName);
    }

    /**
     * 设置Bean属性, 支持 propertyName 多级 ：test.user.name
     *
     * @param bean         bean
     * @param propertyName 属性名
     * @param value        属性值
     */
    public static void setProperty(Object bean, String propertyName, Object value) {
        BeanWrapper beanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(Objects.requireNonNull(bean, "bean Could not null"));
        beanWrapper.setPropertyValue(propertyName, value);
    }

    /**
     * 深度拷贝
     *
     * @param source 待拷贝的对象
     * @return 拷贝之后的对象
     */
    @Nullable
    @SuppressWarnings("unchecked")
    public static <T> T deepClone(@Nullable T source) {
        if (source == null) {
            return null;
        }
        FastByteArrayOutputStream fBos = new FastByteArrayOutputStream(1024);
        try (ObjectOutputStream oos = new ObjectOutputStream(fBos)) {
            oos.writeObject(source);
            oos.flush();
        } catch (IOException ex) {
            throw new IllegalArgumentException("Failed to serialize object of type: " + source.getClass(), ex);
        }
        try (ObjectInputStream ois = new ObjectInputStream(fBos.getInputStream())) {
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException ex) {
            throw new IllegalArgumentException("Failed to deserialize object", ex);
        }
    }

    /**
     * Copy the property values of the given source bean into the target class.
     * <p>Note: The source and target classes do not have to match or even be derived
     * from each other, as long as the properties match. Any bean properties that the
     * source bean exposes but the target bean does not will silently be ignored.
     * <p>This is just a convenience method. For more complex transfer needs,
     *
     * @param source      the source bean
     * @param targetClazz the target bean class
     * @param <T>         泛型标记
     * @return T
     * @throws BeansException if the copying failed
     */
    @Nullable
    public static <T> T copyProperties(@Nullable Object source, Class<T> targetClazz) throws BeansException {
        if (source == null) {
            return null;
        }
        T to = newInstance(targetClazz);
        BeanUtils.copyProperties(source, to);
        return to;
    }

    /**
     * Copy the property values of the given source bean into the target class.
     * <p>Note: The source and target classes do not have to match or even be derived
     * from each other, as long as the properties match. Any bean properties that the
     * source bean exposes but the target bean does not will silently be ignored.
     * <p>This is just a convenience method. For more complex transfer needs,
     *
     * @param sourceList  the source list bean
     * @param targetClazz the target bean class
     * @param <T>         泛型标记
     * @return List
     * @throws BeansException if the copying failed
     */
    public static <T> List<T> copyProperties(@Nullable Collection<?> sourceList, Class<T> targetClazz) throws BeansException {
        if (sourceList == null || sourceList.isEmpty()) {
            return Collections.emptyList();
        }
        List<T> outList = new ArrayList<>(sourceList.size());
        for (Object source : sourceList) {
            if (source == null) {
                continue;
            }
            T bean = BeanUtil.copyProperties(source, targetClazz);
            outList.add(bean);
        }
        return outList;
    }

    /**
     * 将对象装成map形式，map 不可写
     *
     * @param bean 源对象
     * @return {Map}
     */
    @SuppressWarnings("unchecked")
    public static Map<String, Object> toMap(@Nullable Object bean) {
        if (bean == null) {
            return new HashMap<>(0);
        }
        BeanMap beanMap = BeanMap.create(bean);
        Map<String, Object> objectMap = new HashMap<>(beanMap.size());
        for (Object key : beanMap.keySet()) {
            objectMap.put(key.toString(), beanMap.get(key));
        }
        return objectMap;
    }

    /**
     * 将对象装成map形式，map 可写
     *
     * @param bean 源对象
     * @return {Map}
     */
    public static Map<String, Object> toNewMap(@Nullable Object bean) {
        return new HashMap<>(toMap(bean));
    }

    /**
     * 将map 转为 bean
     *
     * @param beanMap   map
     * @param valueType 对象类型
     * @param <T>       泛型标记
     * @return {T}
     */
    public static <T> T toBean(Map<String, Object> beanMap, Class<T> valueType) {
        Objects.requireNonNull(beanMap, "beanMap Could not null");
        T to = newInstance(valueType);
        if (beanMap.isEmpty()) {
            return to;
        }
        BeanMap map = BeanMap.create(to);
        map.putAll(beanMap);
        return to;
    }

    /**
     * 给一个class添加字段
     *
     * @param superclass 父级
     * @param props      新增属性
     * @return {Object}
     */
    public static Object generator(Class<?> superclass, BeanProperty... props) {
        BeanGenerator generator = new BeanGenerator();
        generator.setSuperclass(superclass);
        generator.setUseCache(true);
        for (BeanProperty prop : props) {
            generator.addProperty(prop.getName(), prop.getType());
        }
        return generator.create();
    }

    /**
     * 比较对象
     *
     * @param src  源对象
     * @param dist 新对象
     * @return {BeanDiff}
     */
    public static BeanDiff diff(final Object src, final Object dist) {
        Assert.notNull(src, "diff Object src is null.");
        Assert.notNull(src, "diff Object dist is null.");
        return diff(BeanUtil.toMap(src), BeanUtil.toMap(dist));
    }

    /**
     * 比较Map
     *
     * @param src  源Map
     * @param dist 新Map
     * @return {BeanDiff}
     */
    public static BeanDiff diff(final Map<String, Object> src, final Map<String, Object> dist) {
        Assert.notNull(src, "diff Map src is null.");
        Assert.notNull(src, "diff Map dist is null.");
        // 改变
        Map<String, Object> difference = new HashMap<>(8);
        difference.putAll(src);
        difference.putAll(dist);
        difference.entrySet().removeAll(src.entrySet());
        // 老值
        Map<String, Object> oldValues = new HashMap<>(8);
        difference.keySet().forEach((k) -> oldValues.put(k, src.get(k)));
        BeanDiff diff = new BeanDiff();
        diff.getFields().addAll(difference.keySet());
        diff.getOldValues().putAll(oldValues);
        diff.getNewValues().putAll(difference);
        return diff;
    }

}
